package cn.chenxing.module.file.preprocess;

import cn.chenxing.constant.VenusConstant;
import cn.chenxing.domain.dto.TrackBox;
import cn.chenxing.domain.dto.TrackData;
import cn.chenxing.domain.dto.TrackLine;
import cn.chenxing.domain.dto.TrackPoint;
import cn.chenxing.domain.threadlocal.ThreadData;
import cn.chenxing.utils.CommonUtil;

import java.util.*;

/**
 * @Description 数据预处理
 * @Author maogen.ymg
 * @Date 2020/3/24 16:34
 */
public class DataPreProcessing {
    private DataPreProcessing() {}

    /**
     *  轨迹抽稀 -- Douglas-Peucker压缩
     */
    public static void dpCompression() {
        // 所有用户的数据
        TrackData trackData = ThreadData.get().getTrackData();
        // 用户数据
        List<TrackLine> trackLineList = trackData.getTrackLineList();

        for(int i = 0; i < trackLineList.size(); i++) {
            TrackLine trackLine = trackLineList.get(i);
            List<TrackPoint> primeCoordinate = trackLine.getTrackPointList();
            // ----------------------轨迹压缩-----------------
            List<TrackPoint> compressCoordinate = new ArrayList<>();
            // 设定最大距离误差阈值
            double errorDistMax = 30.0;
            // 第一个点,最后一个点
            compressCoordinate.add(primeCoordinate.get(0));
            compressCoordinate.add(primeCoordinate.get(primeCoordinate.size()-1));
            // 执行压缩算法
            executeDpCompress(primeCoordinate, compressCoordinate, errorDistMax);
            // 压缩后排序(ID从小到大)
            sortFilterArray(compressCoordinate);

            // ----------------------压缩值-----------------
            trackLine.setTrackPointList(compressCoordinate);
            // ----------------------平均误差-----------------
            trackLine.setAverageError(getMeanDistError(primeCoordinate, compressCoordinate));
            // ----------------------压缩率-----------------
            trackLine.setCompressRate(100 - CommonUtil.decimalValue(compressCoordinate.size()*1.0/primeCoordinate.size()*100,2));

            // ----------------------距离，最小经纬度，最大经纬度-----------------
            getTrajectoryInfo(compressCoordinate, trackLine);

            trackLineList.set(i, trackLine);
        }

        trackData.setTrackLineList(trackLineList);
        ThreadData.get().setTrackData(trackData);

    }

    /**
     * 轨迹过滤-- 中值滤波
     * @param compressCoordinate 压缩后的数据
     * @return 滤波的数据
     */
    public static TrackBox medianFiltering1(List<TrackPoint> compressCoordinate, int minute) {
        TrackBox trackBox = new TrackBox();

        // 30分钟的的范围
        int m = 30/minute;

        int size = compressCoordinate.size();
        // 0~m
        List<TrackPoint> filterCoordinate = new ArrayList<>(compressCoordinate.subList(0, m));

        for(int i = m; i < size -m; i++){
            // 2m
            List<TrackPoint> sortList= new ArrayList<>(compressCoordinate.subList(i - m, i + m));

            // Longitude
            sortList.sort((o1, o2) -> {
                Double d1 = o1.getLongitude();
                Double d2 = o2.getLongitude();
                return d1.compareTo(d2);
            });
            double midLongitude = sortList.get(sortList.size()/2).getLongitude();
            // Latitude
            sortList.sort((o1, o2) -> {
                Double d1 = o1.getLatitude();
                Double d2 = o2.getLatitude();
                return d1.compareTo(d2);
            });
            double midLatitude = sortList.get(sortList.size()/2).getLatitude();

            TrackPoint midPoint = new TrackPoint();
            midPoint.setLongitude(midLongitude);
            midPoint.setLatitude(midLatitude);
            midPoint.setId(compressCoordinate.get(i).getId());
            midPoint.setTimeStamp(compressCoordinate.get(i).getTimeStamp());
            filterCoordinate.add(midPoint);

        }
        // ~size
        filterCoordinate.addAll(compressCoordinate.subList(size-m*2, size));
        trackBox.setFilterCoordinate(filterCoordinate);
        return trackBox;
    }

    /**
     * 轨迹过滤-- 中值滤波
     * @param compressCoordinate 压缩后的数据
     * @return 滤波的数据
     */
    public static TrackBox medianFiltering(List<TrackPoint> compressCoordinate, int minute) {
        TrackBox trackBox = new TrackBox();

        // 30分钟的的范围
        int m = 30/minute;
        // 初始滤波范围39.91508,39.93235 116.48572,116.51471
        double startRange = 0.06;
        // 滤波递增范围
        double step = 0.02;
        // 最大滤波范围
        double maxRange = 0.1;
        // 滤波平差最小数目
        int rangeNum = (int) Math.ceil(m/6.0);
        // 连续异常点范围.连续多个轨迹点处于异常状态则判为特征点,不是连续的异常点则过滤
        int outPosRangeCount = 4;
        // 异常点数目
        int outPosCount = 0;
        // 异常点合集
        List<TrackPoint> abnormalPoints = new ArrayList<>();

        int size = compressCoordinate.size();
        // 0~m
        List<TrackPoint> filterCoordinate = new ArrayList<>(compressCoordinate.subList(0, m));

        for(int i = m; i < size -m; i++){
            // 2m
            List<TrackPoint> sortList= new ArrayList<>(compressCoordinate.subList(i - m, i + m));

            TrackPoint midPoint = meanGps(sortList, compressCoordinate.get(i), startRange, step, maxRange, rangeNum);

            if(midPoint != null) {
                midPoint.setId(compressCoordinate.get(i).getId());
                midPoint.setTimeStamp(compressCoordinate.get(i).getTimeStamp());
                filterCoordinate.add(midPoint);
                outPosCount = 0;

            }else {
                outPosCount++;

                // 连续多个特征点，则需一起写入
                if(outPosCount >= outPosRangeCount) {
                    abnormalPoints.add(compressCoordinate.get(i));

                    filterCoordinate.addAll(abnormalPoints);
                    outPosCount = 0;
                    abnormalPoints.clear();
                }else {
                    abnormalPoints.add(compressCoordinate.get(i));
                }
            }


        }
        // ~size
        filterCoordinate.addAll(compressCoordinate.subList(size-m, size));
        trackBox.setFilterCoordinate(filterCoordinate);
        return trackBox;
    }


    private static TrackPoint meanGps(List<TrackPoint> sortList, TrackPoint curPoint, double startRange, double step, double maxRange, int rangeNum) {
        TrackPoint trackPoint = new TrackPoint();
        if(startRange > maxRange){
            return null;
        }
        int count = 0;
        double tempSumLon = 0;
        double tempSumLat = 0;
        for(TrackPoint tp: sortList) {
            if(Math.abs(tp.getLongitude() - curPoint.getLongitude()) < startRange && Math.abs(tp.getLatitude() - curPoint.getLatitude()) < startRange){
                tempSumLon += tp.getLongitude();
                tempSumLat += tp.getLatitude();
                count++;
            }
        }
        // 需要递增
        if(count < rangeNum) {
            trackPoint = meanGps(sortList, curPoint, startRange + step, step, maxRange, rangeNum);
        }else if(count != 0){
            trackPoint.setLongitude(CommonUtil.decimalValue(tempSumLon/count, 5));
            trackPoint.setLatitude(CommonUtil.decimalValue(tempSumLat/count, 5));
        }
        return trackPoint;
    }

    /**
     * 获取轨迹的相关详细信息
     * @param compressCoordinate 压缩后的轨迹
     */
    private static void getTrajectoryInfo(List<TrackPoint> compressCoordinate, TrackLine trackLine) {
        TrackPoint pA = compressCoordinate.get(0);
        // 距离
        double distance = 0.0;
        // 最小经度
        double longitudeMin = pA.getLongitude();
        // 最大经度
        double longitudeMax = pA.getLongitude();
        // 最小纬度
        double latitudeMin = pA.getLatitude();
        // 最大纬度
        double latitudeMax = pA.getLatitude();

        for(int i = 1; i < compressCoordinate.size(); i++) {
            TrackPoint pI = compressCoordinate.get(i);
            // 距离
            distance += getDistance(pA, pI);
            if(longitudeMin > pI.getLongitude()) {
                longitudeMin = pI.getLongitude();
            }
            if(longitudeMax < pI.getLongitude()) {
                longitudeMax = pI.getLongitude();
            }
            if(latitudeMin > pI.getLatitude()) {
                latitudeMin = pI.getLatitude();
            }
            if(latitudeMax < pI.getLatitude()) {
                latitudeMax = pI.getLatitude();
            }
            pA = pI;
        }
        // 距离为米
        trackLine.setDistance(CommonUtil.decimalValue(distance, 0))
                .setLongitudeMin(longitudeMin)
                .setLongitudeMax(longitudeMax)
                .setLatitudeMin(latitudeMin)
                .setLatitudeMax(latitudeMax);

    }

    /**
     * 将过滤后的数据进行ID排序
     * @param compressCoordinate 过滤的数据
     */
    private static void sortFilterArray(List<TrackPoint> compressCoordinate) {
        TrackPoint[] trackPoints = new TrackPoint[compressCoordinate.size()];
        int i = 0;
        for (TrackPoint trackPoint : compressCoordinate) {
            trackPoints[i++] = trackPoint;
        }
        // 进行排序
        Arrays.sort(trackPoints);
        compressCoordinate.clear();
        compressCoordinate.addAll(Arrays.asList(trackPoints));
    }

    /**
     * 执行Douglas-Peucker压缩
     * @param primeCoordinate 原始数据
     * @param compressCoordinate 过滤的数据
     * @param errorDistMax 最大距离误差
     */
    private static void executeDpCompress(List<TrackPoint> primeCoordinate, List<TrackPoint> compressCoordinate, double errorDistMax) {
        // 使用一个点数组接收所有的点坐标(使用ArrayList数组解决下标问题，递归求解时可能出现巨大误差)
        TrackPoint[] trackPoints = new TrackPoint[primeCoordinate.size()];
        int i = 0;
        for (TrackPoint trackPoint : primeCoordinate) {
            trackPoints[i++] = trackPoint;
        }
        // DP压缩算法
        dpCompress(trackPoints, compressCoordinate,0,primeCoordinate.size() - 1, errorDistMax);
    }

    /**
     * 根据最大距离限制，采用DP方法递归的对原始轨迹进行采样，得到压缩后的轨迹
     * @param trackPoints：原始经纬度坐标点数组
     * @param compressCoordinate：保持过滤后的点坐标数组
     * @param start：起始下标
     * @param end：终点下标
     * @param errorDistMax：预先指定好的最大距离误差
     */
    private static void dpCompress(TrackPoint[] trackPoints, List<TrackPoint> compressCoordinate,
                                   int start, int end, double errorDistMax) {
        //递归进行的条件
        if(start < end){
            // 最大距离
            double maxDistance= 0;
            // 当前下标
            int curIndex = 0;
            for(int i = start + 1; i < end; i++){
                // 当前点到对应线段的距离
                double curDistance = distToSegment(trackPoints[start], trackPoints[end],trackPoints[i]);
                // 求出最大距离及最大距离对应点的下标
                if(curDistance > maxDistance){
                    maxDistance = curDistance;
                    curIndex = i;
                }
            }
            // 若当前最大距离大于最大距离误差
            if(maxDistance >= errorDistMax){
                // 将当前点加入到过滤数组中
                compressCoordinate.add(trackPoints[curIndex]);
                //将原来的线段以当前点为中心拆成两段，分别进行递归处理
                dpCompress(trackPoints, compressCoordinate, start, curIndex, errorDistMax);
                dpCompress(trackPoints, compressCoordinate, curIndex, end, errorDistMax);
            }
        }
    }

    /**
     * 使用三角形面积（使用海伦公式求得）相等方法计算点pX到点pA和pB所确定的直线的距离
     * @param pA：起始点
     * @param pB：结束点
     * @param pX：第三个点
     * @return distance：点pX到pA和pB所在直线的距离
     */
    private static double distToSegment(TrackPoint pA, TrackPoint pB, TrackPoint pX) {
        double a = Math.abs(getDistance(pA, pB));
        double b = Math.abs(getDistance(pA, pX));
        double c = Math.abs(getDistance(pB, pX));
        double p = (a+b+c)/2.0;
        double s = Math.sqrt(Math.abs(p*(p-a)*(p-b)*(p-c)));
        return s*2.0/a;
    }

    /**
     * 求两个经纬度点之间的距离
     * @param pA：起始点
     * @param pB：结束点
     * @return distance：距离
     */
    private static double getDistance(TrackPoint pA, TrackPoint pB) {
        double radLatA = angleToRadian(pA.getLatitude());
        double radLatB = angleToRadian(pB.getLatitude());
        double deltaLon = angleToRadian(pB.getLongitude() - pA.getLongitude());
        double top1 = Math.cos(radLatB) * Math.sin(deltaLon);
        double top2 = Math.cos(radLatA) * Math.sin(radLatB) - Math.sin(radLatA) * Math.cos(radLatB) * Math.cos(deltaLon);
        double top = Math.sqrt(top1 * top1 + top2 * top2);
        double bottom = Math.sin(radLatA) * Math.sin(radLatB) + Math.cos(radLatA) * Math.cos(radLatB) * Math.cos(deltaLon);
        double deltaSigma = Math.atan2(top, bottom);
        return deltaSigma * VenusConstant.EARTH_RADIUS_IN_METERS;
    }

    /**
     * 角度转弧度
     * @param angle：角度
     * @return 返回的是弧度
     */
    private static double angleToRadian(double angle) {
        return angle * Math.PI / 180.0;
    }

    /**
     * 求平均距离误差(平均误差为压缩时忽略的那些点到对应线段的距离之和除以总点数得到的数值)
     * @param primeCoordinate：原始数据点坐标
     * @param compressCoordinate：过滤后的数据点坐标
     * @return ：返回平均距离误差
     */
    private static double getMeanDistError(List<TrackPoint> primeCoordinate, List<TrackPoint> compressCoordinate) {
        double sumDist = 0.0;
        for(int i = 1; i < compressCoordinate.size(); i++){
            int start = compressCoordinate.get(i-1).getId();
            int end = compressCoordinate.get(i).getId();
            for(int j = start + 1; j < end; j++){
                sumDist += distToSegment(primeCoordinate.get(start), primeCoordinate.get(end), primeCoordinate.get(j));
            }
        }

        return CommonUtil.decimalValue(sumDist/(primeCoordinate.size()),2);
    }
}
